package handlers;

import java.io.*;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import main.ServerMain;

import events.Event;
import events.Events;
import events.Simulation;
import packets.PacketConstants;

/**
 * ClientHandler is responsible for controlling all the I/O of all clients, each connection
 * creates a new handler which is then responsible for maintain the requests of that client. 
 * 
 * This is threaded so many client's can connect simultaneously without slowing down the 
 * performance of any other client.
 *
 * 
 * @author Oliver Behncke 
 * @author Holly Denton 
 * @author Matthew McCarthy
 * @author John Quinlivan
 *
 */
public class ClientHandler extends Thread {

	/* The remote connection that has been established */
	private Socket client;

	/* Notes the main thread */
	private ServerMain main;

	/* Stream for writing to */
	private OutputStream sendToClient;

	/* Stream for reading from */
	private BufferedInputStream receivedFromClient;

	/* Stores the latest message from the server - must be a "PacketConstant" */
	private byte lastMessageFromClient;
	
	/* Prevents another method access the streams if a process is already occurring */
	private boolean currentlyWorking;

	/**
	 * Constructor, requires access to the master file handler and the socket to which
	 * this client is being handled on
	 * 
	 * @param socket 		A socket where a client should be listening 
	 * @param serverMain 	Point back to the ultimate controller for termination procedure
	 */
	public ClientHandler(Socket socket, ServerMain serverMain) {

		/* Assign the fields */
		client = socket;
		main = serverMain;

		/* Request the streams to which reading/writing is possible */
		try {
			/* Stream that can be written to */
			sendToClient = client.getOutputStream();

			/* Stream that can be read from */
			receivedFromClient = new BufferedInputStream(client.getInputStream());
		} catch (Exception e) {
			System.err.println("\nError getting I/O stream from client: " + client.getInetAddress() + 
					" on port: " + client.getPort() + "!");
			lastMessageFromClient = PacketConstants.TERMINATING;
			return;
		}

		/* Provide some useful output */
		System.err.println("\nEstablished a connection with: " + client.getInetAddress() 
				+ " on remote port: " + client.getPort() + " (local port: " + client.getLocalPort() + ")");
		
		/* Allow modifications */
		currentlyWorking = false;

		/* Update the client with the latest version */
		sendFile("data/database.xml");
	}

	@Override
	public void run(){
		
		/* Await input */
		lastMessageFromClient = PacketConstants.NULL;

		/* Run until terminated */
		while(lastMessageFromClient != PacketConstants.TERMINATED){
			listenForInput();
			respondToInput();
		}
	}

	/**
	 * Whilst the thread is idle, will constantly wait for input from the client-side. 
	 * 
	 * Blocks until input received
	 */
	private void listenForInput() {
		try {
			/* Attempts to read from the client, blocks until complete */
			byte[] info = new byte[1];
			receivedFromClient.read(info);
			lastMessageFromClient = info[0];
		} catch (Exception e) {
			killConnection("Error reading input from remote client! (Client window has probably been closed)");
		}
	}

	/**
	 * Responds to data transfer requests
	 */
	private void respondToInput() {
		if(lastMessageFromClient == PacketConstants.SENDING_DATA){
			receiveEvent();
			lastMessageFromClient = PacketConstants.NULL;
		}
		else if(lastMessageFromClient == PacketConstants.REQUEST_DATABASE){
			sendFile("data/database.xml");
			lastMessageFromClient = PacketConstants.NULL;
		}
	}

	/**
	 * Receives a remote event from this client - updates the database
	 */
	private void receiveEvent(){
		
		/* Stop if the thread is busy elsewhere */
		if(currentlyWorking)
			return;
		
		/* Prevent multiple tasks */
		currentlyWorking = true;

		try{

			/* Compensate lag */
			Thread.sleep(100);

			/* Generate a byte array to poll the expected length of the file from */
			byte[] eventStream = new byte[0];

			while(eventStream.length == 0){
				byte[] lengthOfData = new byte[4];
				receivedFromClient.read(lengthOfData);
				ByteBuffer bb = ByteBuffer.wrap(lengthOfData);

				/* Read expected size of file */
				eventStream = new byte[bb.getInt()];
			}
			
			/* Read the whole stream at once */
			receivedFromClient.read(eventStream);

			JAXBContext context = JAXBContext.newInstance("events");
			Unmarshaller unmarshaller = context.createUnmarshaller();
			Simulation s = (Simulation) unmarshaller.unmarshal(new ByteArrayInputStream(eventStream));
			List<Event> events = s.getPriceOrCostOrMail();
			for(Event e :events)
				Events.saveEvent(e, "data/database.xml");

			/* Allow another task to occur */
			currentlyWorking = false;
	
			/* Update everyone to the changes */
			main.updateAllClients();
		} catch(Exception e){
			e.printStackTrace();
			System.out.println("Error receiving event from client \"" + client.getInetAddress() + "!");
		}
		
		/* Allow another task to occur */
		currentlyWorking = false;
	}

	/**
	 * Sends a file (be it XML or the like) to the remote client 
	 * 
	 * @param pathName Physical location of the file as a String
	 * @return True once a file has been sent - or an error dealt with
	 */
	public boolean sendFile(String pathName) {
		
		/* Stop if the thread is busy elsewhere */
		if(currentlyWorking)
			return false;
		
		/* Prevent multiple tasks */
		currentlyWorking = true;

		/* Inform the UI */
		System.out.println("\nAttempting to send the file: \"" + pathName + "\" to the client: \"" + 
				client.getInetAddress() + "\"");

		/* Generates a file object */
		File fileToSend = new File(pathName);

		/* Checks for validity */
		if(fileToSend.exists()){

			try {

				/* Attempt to load into an input stream */
				FileInputStream arrayCaster = new FileInputStream(fileToSend);

				/* Create the byte array (to be sent) equal to the length of the file */
				byte[] arrayToSend = new byte[(int) fileToSend.length()];

				/* Read the file as a byte array */
				arrayCaster.read(arrayToSend);

				/* Inform the connected pair this is going to send a file */
				sendToClient.write(new byte[] { PacketConstants.SENDING_DATA });

				/* Tell the client how long the file is */
				ByteBuffer arrayLength = ByteBuffer.allocate(4);
				arrayLength.putInt(arrayToSend.length);
				sendToClient.write(arrayLength.array());

				/* Write the file as an array to the stream */
				sendToClient.write(arrayToSend);

				/* Inform the UI */
				System.out.println("Successfully sent file \"" + pathName + "\" to the client: \"" 
						+ client.getInetAddress() + "\"!");
			} catch (Exception e) {
				if(lastMessageFromClient != PacketConstants.TERMINATING)
					System.err.println("Error sending file \"" + pathName + "\" to the remote client \"" 
							+ client.getInetAddress() + "!");
				killConnection("Error sending file \"" + pathName + "\" to remote client");
			}
		}
		
		/* Allow another task to occur */
		currentlyWorking = false;
		return true;
	}

	/**
	 * Method that terminates the connection with the remote peer. Whether forced or not 
	 * @param string A message to display (optional, can be null)
	 */
	private void killConnection(String string) {

		/* Don't perform more than once */
		if(lastMessageFromClient != PacketConstants.TERMINATED){

			/* Inform the UI */
			if(client != null){
				System.err.println("\nConnection with client: " + client.getInetAddress() + " has been terminated!");
				if(string != null)
					System.err.println("Reasoning: \"" + string + "\"");
			}

			/* Close all streams */
			try{
				receivedFromClient.close();
				sendToClient.close();
				client.close();
			} catch(Exception e){
				System.err.println("Not going quietly...");
			}

			/* Null and garbage collect */
			client = null;
			sendToClient = null;
			receivedFromClient = null;
			lastMessageFromClient = PacketConstants.TERMINATED;
			main.removeClientFromValidClients(this);
			System.gc();
		}
	}	
}
